package model;

import gui.NetworkView;
import gui.ViewUpdater;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.InputMismatchException;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Scanner;

import model.NetworkConnection.Side;

import static org.junit.Assert.*;
/**
 * Objects of this class contain information about a network nodes and their connections.  
 */
public class NetworkModel {
	//--------------------------------------------------------------------------------------------------------------
	//--------------------------------------------------------------------------------------------------------------
	//Variables
	/**
	 * The list of nodes associated with the network model.
	 */
	private List<NetworkNode> nodes;
	
	/**
	 * The list of connections associated with the network model.
	 */
	private List<NetworkConnection> connections;
	
	/**
	 * the file associated with the NetworkModel.
	 */
	private String filename;
	
	/**
	 * This variable will be removed later on if there is a better way to track changes.
	 */
	private boolean changed = false;
	
	/**
	 * Allows the model to redraw the view when it changes.
	 */
	private ArrayList<ViewUpdater> views;
	
//	public int test = (int) (Math.random() * 10000);
	
	//--------------------------------------------------------------------------------------------------------------
	//--------------------------------------------------------------------------------------------------------------
	//Constructors
	/**
	 * Creates an empty network model that has a unique default file name and no contents.
	 */
	public NetworkModel() {
		nodes = new ArrayList<NetworkNode>();
		connections = new ArrayList<NetworkConnection>();
	}

	//----------------------------------------
	/**
	 * Reads the specific file and creates a new NetworkModel object that contains all of the 
	 * information in the file. If there is no such file then an exception should be thrown.
	 * @param fileName the name of the file to be read.
	 */
	public NetworkModel(String filename) {
		this.filename = filename;
		nodes = new ArrayList<NetworkNode>();
		connections = new ArrayList<NetworkConnection>();
		views = new ArrayList<ViewUpdater>();
		Scanner scanfile = null;
		try {
			scanfile = new Scanner(new File(filename));
		}
		catch (FileNotFoundException e) {
			changed = true;
			return;
		}
		load(scanfile);
	}
	
	public NetworkModel(String filename, String savedata) {
		this.filename = filename;
		nodes = new ArrayList<NetworkNode>();
		connections = new ArrayList<NetworkConnection>();
		views = new ArrayList<ViewUpdater>();
		load(new Scanner(savedata));
	}

	//--------------------------------------------------------------------------------------------------------------
	//--------------------------------------------------------------------------------------------------------------
	//Methods
	/**
	 * Returns the name of the file associated with this model.
	 */
	public String getFileName() {
		return filename;
	}
	
	//----------------------------------------
	/**
	 * Changes the file name associated with this model.
	 * @param newFileName the new file name
	 */
	public void setFileName(String newFilename) {
		filename = newFilename;
	}

	//----------------------------------------
	/**
	 * Loads the contents of a file to this model.
	 */
	private void load(Scanner scanfile) {
		while(scanfile.hasNext()) {
			try {
				Scanner scanline = new Scanner(scanfile.nextLine());
				if(!scanline.hasNext()) {
					continue;
				}
				if("N".equals(scanline.next())) {	//Node			- N 100 200 "Central"
					double x = scanline.nextDouble();
					double y = scanline.nextDouble();
					scanline.useDelimiter("\"");
					scanline.next();
					String name = scanline.next();
					NetworkNode node = new NetworkNode(name, x, y);
					node.setNetwork(this);
					addNode(node);
				}
				else {								//Connection	- C "Central" R "Authentication server" L
					scanline.useDelimiter("\"");
					scanline.next();
					String node1 = scanline.next();
					scanline.useDelimiter(" ");
					scanline.next();
					Side side1 = NetworkConnection.getSideEnum(scanline.next());
					scanline.useDelimiter("\"");
					scanline.next();
					String node2 = scanline.next();
					scanline.useDelimiter(" ");
					scanline.next();
					Side side2 = NetworkConnection.getSideEnum(scanline.next());
					if(side1 == null || side2 == null) {
						throw new InputMismatchException();
					}
					addConnection(new NetworkConnection(node1, side1, node2, side2, this));
				}
			}
			catch (InputMismatchException e) {
//				e.printStackTrace();
//				System.out.println("Remainder of bad file:");
//				while(scanfile.hasNext()) {
//					System.out.println(scanfile.nextLine());
//				}
			}
			catch (NoSuchElementException e) {
//				e.printStackTrace();
//				System.out.println("Remainder of bad file:");
//				while(scanfile.hasNext()) {
//					System.out.println(scanfile.nextLine());
//				}
			}
		}
		changed = false;
	}
	
	//----------------------------------------
	/**
	 * Saves the contents of this model to its file.
	 */
	public void save() {
		try {
			PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(filename)));
			out.print(savedata());
			out.close();
		}
		catch (IOException e) {
			e.printStackTrace();
		}
		changed = false;
	}
	
	public String savedata() {
		String result = "";
		for(int i = 0; i < nNodes(); i++) {
			result += getNode(i) + "\n";
		}
		for(int i = 0; i < nConnections(); i++) {
			result += getConnection(i) + "\n";
		}
		return result;
	}
	
	//----------------------------------------
	/**
	 * Returns true if there are unsaved changes.
	 */
	public boolean unsavedChanges() {
		//TODO unsavedChanges For project 2, this should always return false as there is nothing to change
		return changed;
	}
	
	//----------------------------------------
	public void affectChange() {
		changed = true;
		for(int i = 0; i < views.size(); i++) {
			views.get(i).updateView();
		}
	}
	
	//----------------------------------------
	/**
	 * Adds the specified NetworkNode to the list of network objects.
	 * @param newNode
	 */
	public void addNode(NetworkObject newNode) {
		if(newNode == null) { // || getNode(newNode.getName()) != null) {
			return;
		}
		nodes.add((NetworkNode)newNode);
		affectChange();
	}
	
	//----------------------------------------
	/**
	 * Returns the number of network node objects in this model.
	 */
	public int nNodes() {
		return nodes.size();
	}
	
	//----------------------------------------
	/**
	 * Returns the specified NetworkNode. Indexes begin at zero.
	 * @param i index of the desired object. Must be less than nNodes()
	 */
	public NetworkNode getNode(int i) {
		if(i < nodes.size() && i >= 0) {
			return nodes.get(i);
		}
		
		return null;
	}
	
	//----------------------------------------
	public NetworkNode getNode(String node) {
		int index = nodes.indexOf(new NetworkNode(node, 0, 0));
		if(index == -1) {
			return null;
		}
		return nodes.get(index);
	}
	
	//----------------------------------------
	/**
	 * Removes the specified object from the list of nodes.
	 * @param i the index of the object to be removed.
	 */
	public void removeNode(int i) {
		if(i < nodes.size()  && i >= 0) {
			NetworkNode node = nodes.get(i);
			nodes.remove(i);
			for(int j = connections.size() - 1; j >= 0; j--) {
				if(connections.get(j).containsNode(node)) {
					connections.remove(j);
				}
			}
		}
		affectChange();
	}
	
	//----------------------------------------
	/**
	 * Adds the specified NetworkConnection to the list of connections.
	 * @param newConnection
	 */
	public void addConnection(NetworkConnection newConnection) {
		if(newConnection == null) {
			return;
		}
		connections.add(newConnection);
		affectChange();
	}
	
	//----------------------------------------
	/**
	 * Returns the number of network connections in this model.
	 */
	public int nConnections() {
		return connections.size();
	}
	
	//----------------------------------------
	/**
	 * Returns the specified NetworkConnection. Indexes begin at zero.
	 * @param i index of the desired object. Must be less than nConnections()
	 */
	public NetworkConnection getConnection(int i) {
		if(i < connections.size() && i >= 0) {
			return connections.get(i);
		}
		
		return null;
	}
	
	//----------------------------------------
	/**
	 * Removes the specified object from the list of connections.
	 * @param i the index of the object to be removed.
	 */
	public void removeConnection(int i) {
		if(i < connections.size() && i >= 0) {
			connections.remove(i);
			affectChange();
		}
	}
	
	//----------------------------------------
	public void addViewListener(ViewUpdater view) {
		views.add(view);
	}
	
	//--------------------------------------------------------------------------------------------------------------
	//--------------------------------------------------------------------------------------------------------------
	//Test
	/**
	* This method is a regression test to verify that this class is
	* implemented correctly. It should test all of the methods including
	* the exceptions. It should be completely self checking. This 
	* should write "testing NetworkModel" to System.out before it
	* starts and "NetworkModel OK" to System.out when the test
	* terminates correctly. Nothing else should appear on a correct
	* test. Other messages should report any errors discovered.
	**/
	public static void Test() {
		boolean result = true;
		System.out.println("testing NetworkModel...");
		
		//--------------------------------------------
		//Reading from a file
		NetworkModel model1 = new NetworkModel("Test1.txt");
		result = result && assertTest(model1.getFileName().equals("Test1.txt"), "Filename was not received correctly.");
		result = result && assertTest(model1.nNodes() == 4, "Incorrect number of nodes. Expected: 4, Given: " + model1.nNodes());
		result = result && assertTest(model1.nConnections() == 5, "Incorrect number of connections. Expected: 5, Given: " + model1.nConnections());
		for(int i = 0; i < model1.nNodes(); i++) {
			result = result && assertTest(model1.getNode(i) != null, "Null node retrieved at index " + i);
		}
		result = result && assertTest(model1.getNode(-1) == null, "Returned non-null node when requesting non-existent node.");
		result = result && assertTest(model1.getNode(model1.nNodes()) == null, "Returned non-null node when requesting non-existent node.");
		
		for(int i = 0; i < model1.nConnections(); i++) {
			result = result && assertTest(model1.getConnection(i) != null, "Null connections retrieved at index " + i);
		}
		result = result && assertTest(model1.getConnection(-1) == null, "Returned non-null connection when requesting non-existent connection.");
		result = result && assertTest(model1.getConnection(model1.nConnections()) == null, "Returned non-null connection when requesting non-existent connection.");
		
		//--------------------------------------------
		//Bad file
		try {
		new NetworkModel("Test2.txt");
		new NetworkModel("Test5.txt");
		new NetworkModel("Badfile.txt");
		}
		catch (Exception e) {
			e.printStackTrace();
			result = result && assertTest(false, "Attempting to load a bad file would have crashed the program.");
		}
		
		//--------------------------------------------
		//Adding/removing nodes/connections
		NetworkModel model2 = new NetworkModel();
		model2.setFileName("Test0.txt");
		result = result && assertTest(model2.getFileName().equals("Test0.txt"), "Filename was not set correctly.");
		model2.addNode(new NetworkNode("node1", 100, 100));
		model2.addNode(new NetworkNode("node2", 100, 200));
		model2.addNode(new NetworkNode("node3", 200, 200));
		model2.addNode(new NetworkNode("node4", 200, 100));
		model2.addNode(new NetworkNode("node4", 200, 100));
		model2.addNode(null);
		result = result && assertTest(model2.nNodes() == 4, "Incorrect number of nodes. Expected: 4, Given: " + model2.nNodes());
		
		model2.addConnection(new NetworkConnection("node1", Side.Right, "node2", Side.Left, model2));
		model2.addConnection(new NetworkConnection("node2", Side.Bottom, "node3", Side.Top, model2));
		model2.addConnection(new NetworkConnection("node3", Side.Left, "node4", Side.Right, model2));
		model2.addConnection(new NetworkConnection("node4", Side.Top, "node1", Side.Bottom, model2));
		model2.addConnection(null);
		result = result && assertTest(model2.nConnections() == 4, "Incorrect number of connections. Expected: 4, Given: " + model2.nConnections());
		
		result = result && assertTest(model2.unsavedChanges(), "Model incorrectly reported no unsaved changes.");
		model2.save();
		result = result && assertTest(!model2.unsavedChanges(), "Model incorrectly reported unsaved changes.");
		
		model2.removeConnection(3);
		model2.removeConnection(6);
		model2.removeConnection(-1);
		result = result && assertTest(model2.nConnections() == 3, "Incorrect number of connections. Expected: 3, Given: " + model2.nConnections());
		model2.removeNode(3);
		model2.removeNode(6);
		model2.removeNode(-1);
		result = result && assertTest(model2.nNodes() == 3, "Incorrect number of nodes. Expected: 3, Given: " + model2.nNodes());
		result = result && assertTest(model2.nConnections() == 2, "Incorrect number of connections. Expected: 2, Given: " + model2.nConnections());
		
		
		result = result && assertTest(model2.unsavedChanges(), "Model incorrectly reported no unsaved changes.");
		model2.save();
		result = result && assertTest(!model2.unsavedChanges(), "Model incorrectly reported unsaved changes.");
		
		//--------------------------------------------
		if(result) {
			System.out.println("NetworkModel OK");
		}
		else {
			System.out.println("One or more tests failed.");
		}
	}
	
	private static boolean assertTest(boolean test, String failureMessage) {
		try {
			assertTrue(test);
			return true;
		}
		catch (AssertionError e) {
			e.printStackTrace();
			System.err.println(failureMessage);
			return false;
		}
	}

	public void removeView(NetworkView view) {
		views.remove(view);
		
	}

	public int numberOfViews() {
		return views.size();
	}
}